//////////////////////////////////////////////////////////////////////////////////
//                                                                              //
//  This file is part of the buola project (https://code.google.com/p/buola/).  //
//                                                                              //
//  Copyright(c) 2007-2012 Xavi Gratal                                          //
//  gratal AT gmail DOT com                                                     //
//                                                                              //
//  Buola is free software: you can redistribute it and/or modify               //
//  it under the terms of the GNU General Public License as published by        //
//  the Free Software Foundation, either version 3 of the License, or           //
//  (at your option) any later version.                                         //
//                                                                              //
//  Buola is distributed in the hope that it will be useful,                    //
//  but WITHOUT ANY WARRANTY; without even the implied warranty of              //
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               //
//  GNU General Public License for more details.                                //
//                                                                              //
//  You should have received a copy of the GNU General Public License           //
//  along with buola.  If not, see <http://www.gnu.org/licenses/>.              //
//                                                                              //
//////////////////////////////////////////////////////////////////////////////////

#include <buola/os/cmimetype.h>
#include <buola/os/capplication.h>
#include <buola/io/curi.h>
#include <buola/io/cfile.h>
#include <buola/algorithm/split.h>
#include <buola/io/glob.h>
#include <buola/io/cresourcefinder.h>
#include <buola/io/ctextreader.h>
#include <buola/io/cbinaryreader.h>
#include <buola/xml.h>
#include <buola/os/cappaction.h>
#include <buola/utility/cdesktopfile.h>
#include <buola/os/cicontheme.h>

namespace buola { namespace os {

recursive_mutex CMIMEType::mStaticMutex;
std::unordered_map<std::string,CMIMEType*> CMIMEType::mTypes;

CMIMEType::EInit CMIMEType::mInit=CMIMEType::INIT_NONE;
std::unordered_map<std::string,std::string> CMIMEType::mAliases;
std::unordered_multimap<std::string,std::string> CMIMEType::mAppCache;
std::vector<CMIMEType::SGlob*> CMIMEType::mGlobs;
std::vector<CMIMEType::SMagic*> CMIMEType::mMagics;

static int sMagicFurthest=0;

struct CMIMEType::SMagic
{
    SMagic(int pPriority,const std::string &pType,io::PBufferStream pStream)
        :   mPriority(pPriority)
        ,   mType(pType)
    {
        io::CTextReader lReader(pStream);
        io::CBinaryReader lBinary(pStream);
        char lByte;

        mPriority=pPriority;
        mType=pType;

        if(!lReader.Peek(lByte))
            throw XData("can't read magic file");

        if(lByte=='>')
        {
            mIndent=0;
            lReader.Bump();
        }
        else
        {
            lReader >> mIndent;
            lReader.Ignore(1,'>');
        }

        lReader >> mOffset;
        lReader.Ignore(1,'=');

        lBinary.ReadInteger(mLength,EEndianness::BIG,2);

        mValue=new char[mLength];
        lReader.Read(mValue,mLength);

        mMask=nullptr;
        mRange=1;

        lReader.Read(&lByte,1);
        while(lByte!='\n')
        {
            if(lByte=='&')
            {
                mMask=new char[mLength];
                lReader.Read(mMask,mLength);
            }
            else if(lByte=='~')
            {
                int lWordSize;
                lReader >> lWordSize;
                if(lWordSize>1)
                {
                    for(int i=0;i<mLength;i+=lWordSize)
                    {
                        if(i+lWordSize>mLength)
                        {
                            lWordSize=mLength-i;
                        }
                        for(int j=0;j<(lWordSize>>1);j++)
                        {
                            char lTemp=mValue[i+lWordSize-j-1];
                            mValue[i+lWordSize-j-1]=mValue[i+j];
                            mValue[i+j]=lTemp;
                        }
                    }
                }
            }
            else if(lByte=='+')
            {
                lReader >> mRange;
            }

            if(lReader.Read(&lByte,1).Count()<1) break;
        }

        if(mOffset+mLength>sMagicFurthest)
            sMagicFurthest=mOffset+mLength;
    }
    
    bool Check(uint8_t *pBuffer,int pSize)
    {
        int i;
        for(int r=0;r<mRange;r++)
        {
            if(mOffset+mLength+r>pSize)
                return false;

            if(mMask)
            {
                for(i=0;i<mLength;i++)
                {
                    if((pBuffer[mOffset+r+i]&mMask[i])!=(mValue[i]&mMask[i]))
                        break;
                }
                if(i<mLength) continue;
            }
            else
            {
                for(i=0;i<mLength;i++)
                {
                    if(pBuffer[mOffset+r+i]!=mValue[i])
                        break;
                }
                if(i<mLength) continue;
            }

            return true;
        }
        return false;
    }

    int mPriority;
    int mLength;
    int mIndent;
    int mOffset;
    int mRange;

    char *mValue;
    char *mMask;

    std::string mType;
};

struct CMIMEType::SGlob
{
    SGlob(const std::string &pGlob)
    {
        int lColon=pGlob.find(':');
        if(lColon<0) throw XData("malformed glob");

        mType=pGlob.substr(0,lColon);
        mExpression=pGlob.substr(lColon+1);
    }

    bool Check(const std::string &pName)
    {
        return io::glob_match(pName,mExpression);
    }

    std::string mExpression;
    std::string mType;
};


CMIMEType::CMIMEType(const std::string &pType)
    :   mType(pType)
{
    io::CURI lURL=io::mime_finder().FindFile(mType);

    xml::CDoc lDoc;
    lDoc.Load(lURL);

    xml::CNode lNode=lDoc.Root();

    if(lNode.Name()!="mime-type") throw XData("malformed mime file");

    for(auto lChild : lNode.Children())
    {
        if(lChild.MatchesName("comment"))
        {
            std::string lLang=lChild.Lang();
            if(lLang.empty()||lLang==ULocale().Name())
            {
                mComment=utf32(lChild.Content());
            }
        }
        else if(lChild.MatchesName("sub-class-of"))
        {
            mSuperClasses.push_back(lChild.GetStringAttrib("type"));
        }
    }

    int lPos=mType.find(L'/');
    mMedia=mType.substr(0,lPos);
    mSubType=mType.substr(lPos+1);
}
 
std::string CMIMEType::GetIcon(os::CIconTheme *pTheme)
{
    auto i=mIcons.find(pTheme);
    if(i!=mIcons.end()) return i->second;
    
    if(mMedia=="inode")
    {
        std::string lIcon;
        
        if(mSubType=="blockdevice")
        {
            lIcon="harddrive";
        }
        else if(mSubType=="chardevice")
        {
            lIcon="chardevice";
        }
        else if(mSubType=="directory")
        {
            lIcon="folder";
        }
        else if(mSubType=="fifo")
        {
            lIcon="gnome-fs-regular";
        }
        else if(mSubType=="mount-point")
        {
            lIcon="gnome-fs-directory";
        }
        else if(mSubType=="socket")
        {
            lIcon="gnome-fs-regular";
        }
        else if(mSubType=="symlink")
        {
            lIcon="emblem-symbolic-link";
        }
        else
        {
            lIcon="gnome-fs-regular";
        }
        
        mIcons[pTheme]=lIcon;
        return lIcon;
    }

    std::vector<std::string> lList;
    lList.push_back(mMedia+"-"+mSubType);
    lList.push_back(mMedia);
    lList.push_back("application-octet-stream");

    std::string lIcon=pTheme->GetFirst(lList,CIconTheme::CONTEXT_MIMETYPES);

    if(lIcon.empty()) throw XNotFound(mType+I18N(": No icon found for MIME type"));
    
    mIcons[pTheme]=lIcon;
    return lIcon;
}

int CMIMEType::FillActionList(std::vector<CFileAction*> &pList)
{
    if(!(mInit&INIT_APPCACHE)) InitAppCache();
    
    auto lPair=mAppCache.equal_range(mType);
    
    int lCount=0;
    for(auto i=lPair.first;i!=lPair.second;++i)
    {
        CApplication *lApp=CApplication::Get(i->second);
        
        for(int j=0;j<lApp->GetActions().size();j++)
        {
            pList.push_back(lApp->GetActions()[j]);
            lCount++;
        }
    }

    for(int i=0;i<mSuperClasses.size();i++)
    {
        lCount+=Get(mSuperClasses[i])->FillActionList(pList);
    }

    return lCount;
}

CMIMEType *CMIMEType::Get(const std::string &pMIME)
{
    lock_guard<recursive_mutex> lLock(mStaticMutex);

    auto i=mTypes.find(pMIME);
    
    if(i!=mTypes.end())
        return i->second;
    
    //try to find it as an alias
    if(!(mInit&INIT_ALIASES)) InitAliases();

    auto a=mAliases.find(pMIME);
    if(a!=mAliases.end())
    {
        CMIMEType *lType=Get(a->second);
        mTypes[pMIME]=lType;
        return lType;
    }

    //if it is not loaded, try to load it
    CMIMEType *lType=new CMIMEType(pMIME);
    mTypes[pMIME]=lType;
    return lType;
}

void CMIMEType::GetAll(std::vector<CMIMEType*> &pCont)
{
    pCont.clear();

    for(auto i=mTypes.begin();i!=mTypes.end();++i)
    {
        pCont.push_back(i->second);
    }
}

std::string CMIMEType::Guess(const io::CFile &pFile)
{
    if(pFile.IsFolder())
    {
        return std::string("inode/directory");
    }
    else if(pFile.IsChar())
    {
        return std::string("inode/chardevice");
    }
    else if(pFile.IsBlock())
    {
        return std::string("inode/blockdevice");
    }
    else if(pFile.IsFifo())
    {
        return std::string("inode/fifo");
    }
    else if(pFile.IsSocket())
    {
        return std::string("inode/socket");
    }
    else if(pFile.IsRegular())
    {
        //Check globs first
        if(!(mInit&INIT_GLOBS)) InitGlobs();

        std::string lName=pFile.URI().Name();

        for(int i=0;i<mGlobs.size();i++)
        {
            if(mGlobs[i]->Check(lName))
            {
                return mGlobs[i]->mType;
            }
        }

        //Then magic
        //maybe the file is not accessible, so catch exceptions
        if(!(mInit&INIT_MAGICS)) InitMagics();

        try
        {
            if(pFile.Size()>0)
            {
                //Zero-length files (files in proc) can block even with O_NONBLOCK
                //for example, /proc/kmsg
                //Check for magic
                io::PStream lStream=pFile.GetStream(io::EMode::OPEN|io::EMode::NONBLOCK);

                uint8_t lBuffer[sMagicFurthest];

                size_t lSize=lStream->Read(lBuffer,sMagicFurthest).Count();

                int lIndent=0;
                SMagic *lFound=nullptr;

                for(int i=0;i<mMagics.size();i++)
                {
                    if(mMagics[i]->mIndent<lIndent)
                    {
                        if(lFound)
                            break;
                        else
                            lIndent=mMagics[i]->mIndent;
                    }
                    else if(mMagics[i]->mIndent>lIndent)
                    {
                        continue;
                    }

                    if(mMagics[i]->Check(lBuffer,lSize))
                    {
                        lFound=mMagics[i];
                        lIndent++;
                    }
                    else
                    {
                        lFound=nullptr;
                    }
                }

                if(lFound)
                {
                    return lFound->mType;
                }

                for(int i=0;i<lSize&&i<128;i++)
                {
                    if(lBuffer[i]<32&&lBuffer[i]!='\n'&&lBuffer[i]!='\r'&&lBuffer[i]!='\t')
                        return std::string("application/octet-stream");
                }
            }
        }
        catch(std::exception&)
        {
        }
    }
    else if(pFile.IsLink())
    {
        return std::string("inode/symlink");
    }

    return std::string("text/plain");
}

std::string CMIMEType::Guess(const io::CURI &pURI,bool pGlobOnly)
{
    if(pGlobOnly)
    {
        if(!(mInit&INIT_GLOBS)) InitGlobs();

        std::string lName=pURI.Name();

        for(int i=0;i<mGlobs.size();i++)
        {
            if(mGlobs[i]->Check(lName))
            {
                return mGlobs[i]->mType;
            }
        }
        
        return std::string("application/octet-stream");
    }
    else
    {
        io::CFile lFile(pURI);

        std::string lString=Guess(lFile);

        return lString;
    }
}

void CMIMEType::InitAliases()
{
    lock_guard<recursive_mutex> lLock(mStaticMutex);
    if(mInit&INIT_ALIASES) return;

    io::CFile lFile(io::data_finder().FindFile("mime/aliases"));
    io::PBufferStream lStream=lFile.GetBufferStream(io::EMode::OPEN|io::EMode::READ);

    std::string lBuffer;

    while(io::get_line(lStream,lBuffer))
    {
        if(!lBuffer.length()) continue;
        if(lBuffer[0]=='#') continue;

        int lSpace=lBuffer.find(' ');
        if(lSpace<0) continue;

        mAliases[lBuffer.substr(0,lSpace)]=lBuffer.substr(lSpace+1);
    }

    mInit|=INIT_ALIASES;
}

void CMIMEType::InitAppCache()
{
    lock_guard<recursive_mutex> lLock(mStaticMutex);
    if(mInit&INIT_APPCACHE) return;

    std::vector<io::CURI> lList=io::data_finder().FindFiles("applications/mimeinfo.cache");

    //process all files
    for(int i=0;i<lList.size();i++)
    {
        CDesktopFile lDesktopFile(lList[i]);

        io::CURI lBasePath=lList[i].GetParent();

        const CDesktopFile::TGroup &lGroup=lDesktopFile.GetGroup("MIME Cache");

        for(auto i=lGroup.begin();i!=lGroup.end();++i)
        {
            //parse applications
            std::vector<std::string> lAppList;
            split(i->second,lAppList,fn::equal_to(';'),nDeleteEmpty);

            for(int j=0;j<lAppList.size();j++)
            {
                if(lAppList[j].size()<9) continue; //we must remove the final .desktop and still get something
                mAppCache.insert(std::make_pair(i->first,lAppList[j].substr(0,lAppList[j].size()-8)));
            }
        }
    }

    mInit|=INIT_APPCACHE;
}

void CMIMEType::InitGlobs()
{
    lock_guard<recursive_mutex> lLock(mStaticMutex);
    if(mInit&INIT_GLOBS) return;

    std::vector<io::CURI> lGlobFiles=io::data_finder().FindFiles("mime/globs");

    for(int i=0;i<lGlobFiles.size();i++)
    {
        std::string lBuffer;
        
        io::CFile lFile(lGlobFiles[i]);
        io::PBufferStream lStream=lFile.GetBufferStream(io::EMode::OPEN|io::EMode::READ);

        while(io::get_line(lStream,lBuffer))
        {
            if(!lBuffer.length()) continue;
            if(lBuffer[0]=='#') continue;

            mGlobs.push_back(new SGlob(lBuffer));
        }
    }

    mInit|=INIT_GLOBS;
}

void CMIMEType::InitMagics()
{
#if 0 ///\todo!!!!!    
    lock_guard<recursive_mutex> lLock(mStaticMutex);
    if(mInit&INIT_MAGICS) return;

    //Init magic
    std::vector<io::CURI> lMagicFiles=io::data_finder().FindFiles("mime/magic");

    for(int i=0;i<lMagicFiles.size();i++)
    {
        io::CFile lFile(lMagicFiles[i]);
        io::PBufferStream lStream=lFile.GetStream(io::EMode::OPEN|io::EMode::READ);

        lStream->Ignore(12);

        std::string lMimeType;
        uint8_t lByte;
        int lPriority;

        for(;;)
        {
            if(lStream->Read(&lByte,1)!=1) break;

            if(lByte=='[')
            {
                (*lStream) >> lPriority;
                io::ignore_input_check(*lStream,':');
                io::string_input(*lStream,lMimeType,']');
                io::ignore_input_check(*lStream,'\n');
            }
            else
            {
                lStream->unget();

                mMagics.push_back(new SMagic(lPriority,lMimeType,*lStream));
            }
        }
        delete lStream;
    }

    mInit|=INIT_MAGICS;
#endif
}

/*namespace os*/ } /*namespace buola*/ }
